/*
Copyright (c) 2009, Young Digital Planet SA
All rights reserved.

* Redistribution and use in source and binary forms, with or without 
  modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this 
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, 
  this list of conditions and the following disclaimer in the documentation 
  and/or other materials provided with the distribution.
* Neither the name of the Young Digital Planet SA nor the names of its 
  contributors may be used to endorse or promote products derived from this 
  software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/
package com.yauthor.feedback
{
	import flash.events.Event;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
	
	import mx.utils.ObjectUtil;

	/**
	 * Event dispatched when some element of an activity is changed by the user. 
	 * Contains possibly the most complete information about the user action (in the form of
	 * properties). This event should give information about the change of the activity state
	 * (result), if an action was correct or not (if possible), what the user has entered
	 * into an activity.
	 * <p><big>Adds the description of the user action/feedback stage model.</big></p>
	 * <!--
	 * Event oznaczający wykonianie jakiegoś elementu ćwiczenia przez 
	 * użytkownika. Zawiera jak najszerszą informację o zdarzeniu użytkownia.
	 * Powinnien określać, czy wydarzyła się zmiana stanu, czy akcja była poprawna
	 * czy nie (wtedy, kiedy jest to możliwe). Określać, co użytkownik wpisał.
	 * -->
	 */
	public class YUserActionEvent extends Event
	{
		/**
		 * YUserActionEvent event identifier.
		 * @eventType userAction
		 */
		public static const USER_ACTION:String = "userAction";
		
		/**
		 * A feedback property: result.
		 */
		public static const FDB_PROP_RESULT:String		= "result";		
		/**
		 * A feedback property: the user action was correct.
		 */
		public static const FDB_PROP_OK:String		= "ok";
		/**
		 * A feedback property: the user action was incorrect.
		 */
		public static const FDB_PROP_WRONG:String	= "wrong";
		/**
		 * A feedback property: all items in a given context were done correctly.
		 * YActivity automatically calculates this property.
		 */
		public static const FDB_PROP_ALLOK:String	= "allOk";
		/**
		 * A feedback property: result has changed. It should come with the
		 * 'ok' and 'wrong' property.
		 */
		public static const FDB_PROP_STATECHANGED:String = "stateChanged";
		/**
		 * An additional property for stateChanged set if a change is the result
		 * of the loaded state.
		 */
		public static const FDB_PROP_STATELOADED:String = "stateLoaded";
		/**
		 * An additional property for stateChanged set if a change is the result
		 * of the reset function.
		 */
		public static const FDB_PROP_RESET:String = "reset";
		/**
		 * A feedback property: answer given by the user.
		 */
		public static const FDB_PROP_TEXT:String	= "text";
		/**
		 * A feedback property: a correct answer that is connected with
		 * this action.
		 */
		public static const FDB_PROP_ANSWERTEXT:String = "answerText";
		/**
		 * A feedback property: the user demanded feedback information.
		 */
		public static const FDB_PROP_DEMAND:String = "demand";
		
		/**
		 * Place properties. They are returned only when an object asking
		 * for properties and an object which added these properties are the
		 * same.
		 * <!--
		 * Właściwości miejscowe. Zwracane są tylko wtedy, gdy obiekt 
		 * pytający o property i definiujący te property jest ten sam.
		 * -->
		 * @see #getProperties()
		 */
		protected static const IN_PLACE_PROPERTIES:Object = 
			{	"allOk": true, 
				"complete":true,
				"result":true,
				"done":true,
				"errors":true };
		
		/**
		 * The list of property sources in the order in which they are added
		 */
		[ArrayElementType("Object")]
		internal var m_sources:Array;
		/**
		 * Properties by the place where they are defined
		 */
		internal var m_src2props:Dictionary;
		/**
		 * The list of all actions in the order in which thay are added
		 */
		[ArrayElementType("com.yauthor.feedback.IFeedbackAction")] 
		internal var m_actions:Array;
		/**
		 * Sources for an action
		 */
		internal var m_feedback2src:Dictionary;
		
		/**
		 * Cache storing calculated properties set for objects
		 */
		internal var m_effectiveProperts:Dictionary;
		internal var m_source:*;
		/**
		 * Creates an event about the user action
		 * @param prop properties describing an action, a copy of this object is stored
		 * @param src the source of an action
		 * @param bubbles determines whether the event object participates in the bubbling stage of 
		 * the event flow. The default value is true.
		 */
		public function YUserActionEvent( props:Object, src:*, bubbles:Boolean = true ) {
			super( USER_ACTION, bubbles, true );
			m_source = src;
			m_sources = [];
			m_src2props = new Dictionary();
			m_actions = [];
			m_feedback2src = new Dictionary();
			m_effectiveProperts = new Dictionary();
			
			m_sources.push( src );
			m_src2props[src] = ObjectUtil.copy( props );
		}
		
		public function get source():Object {
			return m_source;
		}
		
		/**
		 * Adds properties describing feedback
		 * @param props additional properties
		 * @param src an object that defines properties, null means currentTarget
		 */
		public function appendProperties( props:Object, src:* = null ):void {
			src = findSrc( src );
			// a change of a property invalidates cached values
			// I suppose there is a stage process, I do not have to invalidate everything 
			delete m_effectiveProperts[src];
			var storedProps:Object = m_src2props[src];
			if ( storedProps==null ) {
				m_sources.push( src );
				m_src2props[src] = props;
			} else {
				for( var n:String in props )
					storedProps[n] = props[n];
			}
		}
		
		/**
		 * Gets a complete set of properties describing feedback in the context
		 * of a given object.
		 * @param target properties context, null means currentTarget
		 * @see #IN_PLACE_PROPERTIES
		 * @see #appendProperties()
		 */
		public function getProperties( target:* = null ):Object {
			target = findSrc( target );
			var res:Object = m_effectiveProperts[target];
			if ( res!=null )
				return res;
			
			res = { };
			for( var i:int = 0; i<m_sources.length; ++i ) {
				var src:* = m_sources[i];
				var props:Object = m_src2props[src];
				
				for( var n:String in props ) {
					if ( IN_PLACE_PROPERTIES[n]!=true || target===src )
						res[n] = props[n];
				}
			}
			
			m_effectiveProperts[target] = res;
			return res;
		}
		
		/**
		 * Adds feedback for this event. An object processing an event should first 
		 * check his actions with the event and add actions that match the event.
		 * @param action action - response to the user event
		 * @param src a source of an action, null means currentTarget
		 */
		public function appendAction( action:IFeedbackAction, src:* = null ):void {
			src = findSrc( src );
			
			m_feedback2src[action] = src;
			m_actions.push( action );
		}
		
		/**
		 * Checks if a given action comes from a given source
		 * @param an action response to the user event
		 * @param src a source of an ction
		 */ 
		public function isActionFromSource(action:IFeedbackAction, src:*):Boolean {
			if (m_feedback2src[action] == src) return true;
			return false;
		}
		
		/**
		 * Merges a given event to this event. Actions and properties references 
		 * are copied to this event.
		 * @param evt a source of actions and properties 
		 */
		public function mergeEvent( evt:YUserActionEvent ):void {
			for each( var n:* in evt.m_sources )
				appendProperties( evt.m_src2props[n], n );
			for each( var act:IFeedbackAction in evt.m_actions )
				appendAction( act, evt.m_feedback2src[act] );
		}
		
		
		/**
		 * Calls a given callback for every action in this event. If a callback 
		 * returns true action it is considered handled and is removed from the list 
		 * of actions.
		 * @param callback a function with signature 
		 * <pre>function callback( 
		 * 		evt:YUserActionEvent, 
		 * 		fdb:IFeedbackAction, 
		 * 		src:* ):Boolean</pre>
		 * where<ul>
		 * <li>evt - this object
		 * <li>fdb - action
		 * <li>src - action source 
		 * </ul>
		 * @return information if any action was processed
		 */
		public function processFeedbacks( callback:Function ):Boolean {
			var  uaEvent:YUserActionEvent = this;
			var notHandledActions:Array = m_actions.filter( 
				function (item:IFeedbackAction, index:int, array:Array):Boolean {
					var src:* = m_feedback2src[item];
					return !callback( uaEvent, item, src );
				} );
			
			var res:Boolean = m_actions.length!=notHandledActions.length;
			m_actions = notHandledActions;
			
			return res;
		}
		
		/**
		 * Store sthis event in a given data output. There is the assumption
		 * that all event sources registered within this event have a
		 * property name, which is used to store references to these
		 * objects.
		 * @param out stream where store feedback data
		 * @see #restore()
		 */
		public function store( out:IDataOutput ):void {
			out.writeUnsignedInt( m_sources.length );
			for each( var src:* in m_sources )
				out.writeUTF( src.name );
			
			var src2props:ByteArray = new ByteArray();
			var cnt:uint = 0;
			for( var n:* in m_src2props ) {
				src2props.writeUnsignedInt( m_sources.indexOf(n) );
				src2props.writeObject( m_src2props[n] );
				++cnt;
			}
			out.writeUnsignedInt( cnt );
			out.writeBytes( src2props );
			out.writeUnsignedInt( m_actions.length );
			for each( var act:IFeedbackAction in m_actions )
				out.writeObject( act );
			
			var feedback2src:ByteArray = new ByteArray();
			cnt = 0;
			for( var anAction:* in m_feedback2src ) {
				var actionIndex:uint = m_actions.indexOf(anAction);
				feedback2src.writeUnsignedInt( actionIndex );
				feedback2src.writeUnsignedInt( m_sources.indexOf(m_feedback2src[anAction]) );
				++cnt;
			}
			out.writeUnsignedInt( cnt );
			out.writeBytes( feedback2src );
		}
		
		/**
		 * Restores the userAction event from a given input stream
		 * @param din an input source
		 * @param resolve a function with singature 
		 * <pre>function resolve( name:String )</pre>
		 * returns an event source with a given name
		 * @see #store()
		 */
		public function restore( din:IDataInput, resolve:Function ):void {
			m_sources = [];
			m_src2props = new Dictionary();
			
			var len:uint = din.readUnsignedInt();
			for( var i:uint = 0; i<len; ++i )
				m_sources.push( resolve(din.readUTF()) );
			
			len = din.readUnsignedInt();
			for( i = 0; i<len; ++i )
				m_src2props[m_sources[din.readUnsignedInt()]] =
					din.readObject();
			
			len = din.readUnsignedInt();
			for( i = 0; i<len; ++i )
				m_actions.push( din.readObject() );
			
			len = din.readUnsignedInt();
			for( i = 0; i<len; ++i )
				m_feedback2src[m_actions[din.readUnsignedInt()]] =
					m_sources[din.readUnsignedInt()];
		}
		
		/**
		 * Gets an array of all feedback sources.
		 */ 
		public function get sources():Array {
			return m_sources;
		}
		
		override public function clone():Event {
			var cloneObj:YUserActionEvent = new YUserActionEvent( {}, this.source, this.bubbles );
			cloneObj.m_actions = m_actions;
			cloneObj.m_effectiveProperts = m_effectiveProperts;
			cloneObj.m_feedback2src = m_feedback2src;
			cloneObj.m_sources = m_sources;
			cloneObj.m_src2props = m_src2props;
			
			return cloneObj;
		}
		
		/**
		 * @private
		 */
		protected function findSrc( src:* ):* {
			if ( src==null )
				src = currentTarget;
			if ( src==null )
				throw new ArgumentError( "src cannot be null" );
			
			return src;
		}
	}
}
